import {
  useState,
  useEffect,
  createElement,
  forwardRef,
  ForwardRefExoticComponent,
  JSX,
  useImperativeHandle,
  useCallback,
  RefAttributes,
} from "react";
import { UnityInstance } from "../types/unity-instance";
import { UnityProps } from "../types/unity-props";
import { useCanvasIdentifier } from "../hooks/use-canvas-identifier";
import { useUnityLoader } from "../hooks/use-unity-loader";
import { UnityArguments } from "../types/unity-arguments";

const Unity: ForwardRefExoticComponent<
  UnityProps & RefAttributes<HTMLCanvasElement>
> = forwardRef<HTMLCanvasElement, UnityProps>(
  /**
   * @param unityProps The Unity props provided the the Unity component.
   * @param forwardedRef The forwarded ref to the Unity component.
   * @returns The Unity canvas renderer.
   */
  (props, forwardedRef): JSX.Element => {
    // State to hold the canvas reference and Unity instance.
    // The canvas reference is used to render the Unity instance.
    const [canvasRef, setCanvasRef] = useState<HTMLCanvasElement | null>(null);
    const [unityInstance, setUnityInstance] = useState<UnityInstance | null>(
      null
    );

    // Use a custom hook to generate a unique canvas ID or use the provided one.
    // This ensures that each Unity instance has a unique canvas ID.
    // This is important for multiple Unity instances on the same page.
    // The hook also provides a function to refresh the canvas ID if needed.
    const [canvasId, refreshCanvasId] = useCanvasIdentifier(props.id);

    // Use a custom hook to load the Unity loader script.
    // This hook returns the status of the loader, which can be used to
    // determine if the Unity instance is ready to be initialized.
    const unityLoaderStatus = useUnityLoader(props.unityProvider.loaderUrl);

    /**
     * Callback function to handle the Unity loading progression.
     * This function is called by the Unity loader to update the loading
     * progression of the Unity instance.
     * @param progress The loading progression of the Unity instance.
     */
    const onUnityProgress = useCallback(
      (progress: number) => {
        // This function is called to update the loading progression of the Unity
        // instance.
        props.unityProvider.setLoadingProgression(progress);
        if (progress === 1) {
          // If the loading progression reaches 100%, we can set the isLoaded state
          // to true.
          props.unityProvider.setIsLoaded(true);
        }
      },
      [props.unityProvider]
    );

    // Effect to initialize the Unity instance when the component mounts or
    // when the canvas reference or Unity loader status changes.
    useEffect(() => {
      // Function to initialize the Unity instance.
      // This function is called when the component mounts or when the
      // canvas reference or Unity loader status changes.
      const initializeUnity = async () => {
        if (!canvasRef || unityInstance || unityLoaderStatus !== "Loaded") {
          // If there is no canvas reference, or if the Unity instance is already
          // initialized, or if the Unity loader is not ready yet, we simply return.
          // This prevents unnecessary re-initialization of the Unity instance.
          return;
        }

        console.log("React Unity WebGL: Initializing Unity instance...");

        // Remove the reference to the previous Unity instance if it exists and
        // set the Unity instance in the Unity provider to null.
        props.unityProvider.setUnityInstance(null);
        setUnityInstance(null);

        // Reset the loading progression and isLoaded state to their initial
        // values. This is important to ensure that the Unity context is
        // properly reset when the Unity instance is detached.
        props.unityProvider.setLoadingProgression(0);
        props.unityProvider.setIsLoaded?.(false);
        props.unityProvider.setInitialisationError(undefined);

        // Create a new canvas element with the unique ID.
        // This ensures that the Unity instance is rendered in the correct canvas.
        // The canvas element is created with the ID provided in the props or a
        // unique ID generated by the useCanvasIdentifier hook.
        refreshCanvasId();

        // Create a Unity instance using the createUnityInstance function.
        const unityArguments: UnityArguments = {
          companyName: props.unityProvider.companyName,
          productName: props.unityProvider.productName,
          productVersion: props.unityProvider.productVersion,
          dataUrl: props.unityProvider.dataUrl,
          frameworkUrl: props.unityProvider.frameworkUrl,
          codeUrl: props.unityProvider.codeUrl,
          workerUrl: props.unityProvider.workerUrl,
          memoryUrl: props.unityProvider.memoryUrl,
          symbolsUrl: props.unityProvider.symbolsUrl,
          streamingAssetsUrl: props.unityProvider.streamingAssetsUrl,
          devicePixelRatio: props.devicePixelRatio,
          webglContextAttributes: props.unityProvider.webglContextAttributes,
          cacheControl: props.unityProvider.cacheControl,
          autoSyncPersistentDataPath:
            props.unityProvider.autoSyncPersistentDataPath,
          matchWebGLToCanvasSize: props.matchWebGLToCanvasSize,
          disabledCanvasEvents: props.disabledCanvasEvents,
        };

        // Remove properties that are null or undefined. This is important to
        // avoid passing undefined properties to the Unity instance, which can
        // cause errors during initialization.
        Object.keys(unityArguments).forEach((key) => {
          if (
            unityArguments[key as keyof UnityArguments] === null ||
            unityArguments[key as keyof UnityArguments] === undefined
          ) {
            delete unityArguments[key as keyof UnityArguments];
          }
        });

        // The createUnityInstance function is provided by the Unity loader script.
        // It initializes the Unity instance with the provided canvas and arguments.
        // The function returns a Promise that resolves to the Unity instance.
        // We await the Promise to get the Unity instance and set it in the state.
        // This allows us to use the Unity instance in the component.
        try {
          const unityInstance = await window.createUnityInstance(
            canvasRef,
            unityArguments,
            onUnityProgress
          );

          // If the Unity instance is successfully created, we set it in the state.
          // This allows us to use the Unity instance in the component.
          setUnityInstance(unityInstance);
          // We also set the Unity instance in the Unity provider.
          // This allows the Unity provider to access the Unity instance and
          // call its internal methods.
          props.unityProvider.setUnityInstance(unityInstance);
        } catch (error) {
          // If there is an error during the initialization, we log it to the console.
          // This is important for debugging purposes.
          console.error(
            "React Unity WebGL: Error initializing Unity instance:",
            error
          );

          // We also set the initialisation error in the Unity provider.
          // This allows the parent component to handle the error if needed.
          // The initialisation error can be used to display an error message or
          // take other actions based on the error.
          props.unityProvider.setInitialisationError(error as Error);
        }
      };

      // Function to detach the Unity instance and clean up the canvas.
      // This function is called when the component unmounts or when the
      // Unity instance is no longer needed.
      const detachUnity = async () => {
        if (!unityInstance || !canvasRef) {
          // If there is no Unity instance or canvas reference available,
          // we simply return to avoid any errors.
          return;
        }

        console.log("React Unity WebGL: Detaching Unity instance...");

        // Remove the reference to the previous Unity instance if it exists and
        // set the Unity instance in the Unity provider to null.
        props.unityProvider.setUnityInstance(null);
        setUnityInstance(null);

        // Reset the loading progression and isLoaded state to their initial
        // values. This is important to ensure that the Unity context is
        // properly reset when the Unity instance is detached.
        props.unityProvider.setLoadingProgression(0);
        props.unityProvider.setIsLoaded?.(false);
        props.unityProvider.setInitialisationError(undefined);

        // Create a new canvas element to clean up the Unity instance.
        // This is necessary to ensure that the Unity instance is properly
        // disposed of and the canvas is removed from the DOM.
        // The new canvas element is created with the same ID as the original
        // canvas element, but with a special attribute to indicate that it is
        // a cleanup canvas.
        const cleanupCanvasRef = document.createElement("canvas");
        cleanupCanvasRef.id = canvasRef.id;
        cleanupCanvasRef.setAttribute("react-unity-webgl-role", "cleanup");
        cleanupCanvasRef.style.display = "none";
        document.body.appendChild(cleanupCanvasRef);
        unityInstance.Module.canvas = cleanupCanvasRef;
        setUnityInstance(null);
        await unityInstance.Quit();
        document.body.removeChild(cleanupCanvasRef);
      };

      // Initialize the Unity instance when the component mounts or when the
      // canvas reference or Unity loader status changes.
      initializeUnity();

      return () => {
        // Cleanup the Unity instance and canvas when the component unmounts.
        // This ensures that the Unity instance is properly disposed of and
        // the canvas is removed from the DOM.
        detachUnity();
      };
    }, [canvasRef, unityInstance, unityLoaderStatus, props.unityProvider]);

    // Use the forwarded ref to expose the canvas reference to the parent
    // component. This allows the parent component to access the canvas element
    // directly if needed, for example, to manipulate the canvas or pass it to
    // other libraries.
    useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
      forwardedRef,
      () => canvasRef
    );

    // If the Unity instance is not ready yet, we return a placeholder canvas
    // element with the unique ID. This canvas will be replaced by the Unity
    // instance once it is initialized.
    return createElement("canvas", {
      ref: setCanvasRef,
      id: canvasId,
      style: props.style,
      className: props.className,
      tabIndex: props.tabIndex,
    });
  }
);

export { Unity };
